<!doctype html>
<html>
<head>
  <title>Pointer Events properties tests</title>
  <meta name="viewport" content="width=device-width">
  <meta name="variant" content="?mouse">
  <meta name="variant" content="?pen">
  <meta name="variant" content="?mouse-right">
  <meta name="variant" content="?pen-right">
  <meta name="variant" content="?touch">
  <meta name="variant" content="?mouse-nonstandard">
  <meta name="variant" content="?pen-nonstandard">
  <meta name="variant" content="?mouse-right-nonstandard">
  <meta name="variant" content="?pen-right-nonstandard">
  <meta name="variant" content="?touch-nonstandard">
  <link rel="stylesheet" type="text/css" href="pointerevent_styles.css">
  <style>
    html {
      touch-action: none;
    }

    div {
      padding: 0;
    }

    #square1 {
      background-color: green;
      border: 1px solid black;
      height: 50px;
      width: 50px;
      margin-bottom: 3px;
      display: inline-block;
    }

    #innerFrame {
      position: relative;
      margin-bottom: 3px;
      margin-left: 0;
      top: 0;
      left: 0;
    }
  </style>
</head>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="/resources/testdriver.js"></script>
<script src="/resources/testdriver-actions.js"></script>
<script src="/resources/testdriver-vendor.js"></script>
<!-- Additional helper script for common checks across event types -->
<script type="text/javascript" src="pointerevent_support.js"></script>
<script>
let frameLoaded = undefined;
const frameLoadedPromise = new Promise(resolve => {
  frameLoaded = resolve;
});
</script>
<body>
  <div id="square1"></div>
  <div>
    <iframe onLoad = "frameLoaded()" id="innerFrame" srcdoc='
      <style>
        html {
          touch-action: none;
        }
        #square2 {
          background-color: green;
          border: 1px solid black;
          height: 50px;
          width: 50px;
          display: inline-block;
        }
      </style>
      <body>
        <div id="square2"></div>
      </body>
    '></iframe>
  </div>
  <!-- Used to detect a sentinel event. Once triggered, all other events must
       have been processed. -->
  <div>
    <button id="done">done</button>
  </div>
</body>
<script>
  window.onload = runTests();

  async function runTests() {

    const queryStringFragments = location.search.substring(1).split('-');
    const pointerType = queryStringFragments[0];
    const button = queryStringFragments[1] === "right" ? "right" : undefined;
    const standard = !(queryStringFragments[queryStringFragments.length - 1] === "nonstandard");

    const eventList = [
      'pointerover',
      'pointerenter',
      'pointerdown',
      'pointerup',
      'pointerout',
      'pointerleave',
      'pointermove'
    ];

    function injectScrubGesture(element) {
      const doneButton = document.getElementById('done');
      const actions = new test_driver.Actions();

      let buttonArguments =
        (button == 'right') ? { button: actions.ButtonType.RIGHT }
                            : undefined;

      // The following comments refer to the first event of each type since
      // that is what is being validated in the test.
      return actions
        .addPointer('pointer1', pointerType)
        // The pointermove, pointerover and pointerenter events will be
        // triggered here with a hover pointer.
        .pointerMove(0, -20, { origin: element })
        // Pointerdown triggers pointerover, pointerenter with a non-hover
        // pointer type.
        .pointerDown(buttonArguments)
        // This move triggers pointermove with a non-hover pointer-type.
        .pointerMove(0, 20, { origin: element })
        // The pointerout and pointerleave events are triggered here with a
        // touch pointer.
        .pointerUp(buttonArguments)
        // An addition move outside of the target bounds is required to trigger
        // pointerout & pointerleave events with a hover pointer.
        .pointerMove(0, 0)
        .send();
    }

    // Processing a click or tap on the done button is used to signal that all
    // other events should have beem handled. This is used to catch unhandled
    // events that would otherwise result in a timeout.
    function clickOrTapDone() {
      const doneButton = document.getElementById('done');
      const pointerupPromise = getEvent('pointerup', doneButton);
      const actionPromise = new test_driver.Actions()
        .addPointer('pointer1', 'touch')
        .pointerMove(0, 0, {origin: doneButton})
        .pointerDown()
        .pointerUp()
        .send();
      return actionPromise.then(pointerupPromise);
    }

    function verifyButtonAttributes(event) {
      let downButton, upButton, downButtons, upButtons;
      if (button == 'right') {
         downButton = 2;
         downButtons = 2;
         upButton = 2;
         upButtons = 0;
      } else {
        // defaults to left button click
        downButton = 0;
        downButtons = 1;
        upButton = 0;
        upButtons = 0;
      }
      const expectationsHover = {
        // Pointer over, enter, and move are processed before the button press.
        pointerover:  { button: -1, buttons: 0 },
        pointerenter: { button:  -1, buttons: 0 },
        pointermove:  { button:  -1, buttons: 0 },
        // Button status changes on pointer down and up.
        pointerdown:  { button:  downButton, buttons: downButtons },
        pointerup:    { button:  upButton, buttons: upButtons },
        // Pointer out and leave are processed after the button release.
        pointerout:   { button:  -1, buttons: 0 },
        pointerleave: { button:  -1, buttons: 0 }
      };
      const expectationsNoHover = {
        // We don't see pointer events except during a touch gesture.
        // Move is the only pointer event where the "button" click state is not
        // changing. All other pointer events are associated with the start or
        // end of a touch gesture.
        pointerover:  { button:  0, buttons: 1 },
        pointerenter: { button:  0, buttons: 1 },
        pointerdown:  { button:  0, buttons: 1 },
        pointermove:  { button: -1, buttons: 1 },
        pointerup:    { button:  0, buttons: 0 },
        pointerout:   { button:  0, buttons: 0 },
        pointerleave: { button:  0, buttons: 0 }
      };
      const expectations =
          (pointerType == 'touch') ? expectationsNoHover : expectationsHover;

      assert_equals(event.button, expectations[event.type].button,
                    `Button attribute on ${event.type}`);
      assert_equals(event.buttons, expectations[event.type].buttons,
                    `Buttons attribute on ${event.type}`);
    }

    function verifyPosition(event) {
      const boundingRect = event.target.getBoundingClientRect();

      // With a touch pointer type, the pointerout and pointerleave will trigger
      // on pointerup while clientX and clientY are still within the target's
      // bounds. With a hover pointer, these events will be triggered only after
      // clientX or clientY are out of the target's bounds.
      if (pointerType != 'touch' &&
          (event.type == 'pointerout' || event.type == 'pointerleave')) {
        assert_true(
            boundingRect.left > event.clientX ||
            boundingRect.right < event.clientX ||
            boundingRect.top > event.clientY ||
            boundingRect.bottom < event.clientY,
            `clientX/clientY is outside the element bounds for ${event.type} event`);
      } else {
        assert_true(
            boundingRect.left <= event.clientX &&
            boundingRect.right >= event.clientX,
            `clientX is within the expected range for ${event.type} event`);
        assert_true(
            boundingRect.top <= event.clientY &&
            boundingRect.bottom >= event.clientY,
            `clientY is within the expected range for ${event.type} event`);
      }
    }

    function verifyEventAttributes(event, testNamePrefix) {
       verifyButtonAttributes(event);
       verifyPosition(event);
       assert_true(event.isPrimary, 'isPrimary attribute is true');
       check_PointerEvent(event, testNamePrefix, standard);
    }

    function pointerPromise(test, testNamePrefix, type, target) {
      let rejectCallback = undefined;
      promise = new Promise((resolve, reject) => {
        // Store a reference to the promise rejection functions, which would
        // otherwise not be visible outside the promise object. If the callback
        // remains set when the deadline is reached, it means that the promise
        // will not get resolved and should be rejected.
        rejectCallback = reject;
        const pointerEventListener = event => {
          rejectCallback = undefined;
          assert_equals(event.type, type, `type attribute for ${type} event`);
          event.preventDefault();
          resolve(event);
        };
        target.addEventListener(type, pointerEventListener, { once: true });
        test.add_cleanup(() => {
          // Just in case of an assert prior to the events being triggered.
          document.removeEventListener(type, pointerEventListener,
                                       { once: true });
        });
      }).then(result => { verifyEventAttributes(result, testNamePrefix); },
              error => { assert_unreached(error); });
      promise.deadlineReached = () => {
        // If the event has not been received, the promise will not be
        // fulfilled, leading to a timeout. Reject the promise if still pending.
        if (rejectCallback) {
          rejectCallback(`missing ${type} event`);
        }
      }
      return promise;
    }

    async function runPointerEventsTest(test, testNamePrefix, target) {
      assert_true(['mouse', 'pen', 'touch'].indexOf(pointerType) >= 0,
                  `Unexpected pointer type (${pointerType})`);

      const promises = [];
      eventList.forEach(type => {
        // Create a promise for each event type. If clicking on the done button
        // is detected before an event's promise is resolved, then the promise
        // will be rejected. Otherwise, the attributes for the event are
        // verified.
        promises.push(pointerPromise(test, testNamePrefix, type, target));
      });

      await injectScrubGesture(target);

      // The injected gestures consist of a shrub on a button followed by a
      // click on the done button. The promise is only resolved after the
      // done click is detected. At this stage all other events must have been
      // processed. Any unresolved promises in the list will be rejected to
      //  avoid a test timeout. The rejection will trigger a test failure.
      await clickOrTapDone().then(promises.map(p => p.deadlineReached()));

      // Once all promises are resolved, all event attributes have been
      // successfully verified.
      return Promise.all(promises);
    }

    promise_test(t => {
      const square1 = document.getElementById('square1');
      return runPointerEventsTest(t, '', square1);
    }, 'Test pointer events in the main document');

    promise_test(async t => {
      const innerFrame = document.getElementById('innerFrame');
      await frameLoadedPromise;
      const square2 = innerFrame.contentDocument.getElementById('square2');
      return runPointerEventsTest(t, 'Inner Frame', square2);
    }, 'Test pointer events in an iframe');
  }
</script>
